/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.util.hll;

import com.carrotsearch.randomizedtesting.RandomizedTest;
import java.util.Random;
import org.apache.solr.SolrTestCase;
import org.junit.Test;

/** Unit and smoke tests for {@link BigEndianAscendingWordDeserializer}. */
public class BigEndianAscendingWordDeserializerTest extends SolrTestCase {
  /** Error checking tests for constructor. */
  @Test
  public void constructorErrorTest() {
    // word length too small
    IllegalArgumentException e =
        expectThrows(
            IllegalArgumentException.class,
            () -> {
              new BigEndianAscendingWordDeserializer(
                  0 /*wordLength, below minimum of 1*/,
                  0 /*bytePadding, arbitrary*/,
                  new byte[1] /*bytes, arbitrary, not used here*/);
            });
    assertTrue(e.getMessage().contains("Word length must be"));

    // word length too large
    e =
        expectThrows(
            IllegalArgumentException.class,
            () -> {
              new BigEndianAscendingWordDeserializer(
                  65 /*wordLength, above maximum of 64*/,
                  0 /*bytePadding, arbitrary*/,
                  new byte[1] /*bytes, arbitrary, not used here*/);
            });
    assertTrue(e.getMessage().contains("Word length must be"));

    // byte padding negative
    e =
        expectThrows(
            IllegalArgumentException.class,
            () -> {
              new BigEndianAscendingWordDeserializer(
                  5 /*wordLength, arbitrary*/,
                  -1 /*bytePadding, too small*/,
                  new byte[1] /*bytes, arbitrary, not used here*/);
            });
    assertTrue(e.getMessage().contains("Byte padding must be"));
  }

  /** Smoke test using 64-bit short words and special word values. */
  @Test
  public void smokeTest64BitWord() {
    final BigEndianAscendingWordSerializer serializer =
        new BigEndianAscendingWordSerializer(
            64 /*wordLength*/, 5 /*wordCount*/, 0 /*bytePadding, arbitrary*/);

    // Check that the sign bit is being preserved.
    serializer.writeWord(-1L);
    serializer.writeWord(-112894714L);

    // Check "special" values
    serializer.writeWord(0L);
    serializer.writeWord(Long.MAX_VALUE);
    serializer.writeWord(Long.MIN_VALUE);

    final byte[] bytes = serializer.getBytes();

    final BigEndianAscendingWordDeserializer deserializer =
        new BigEndianAscendingWordDeserializer(64 /*wordLength*/, 0 /*bytePadding*/, bytes);

    assertEquals(deserializer.totalWordCount(), 5 /*wordCount*/);

    assertEquals(deserializer.readWord(), -1L);
    assertEquals(deserializer.readWord(), -112894714L);
    assertEquals(deserializer.readWord(), 0L);
    assertEquals(deserializer.readWord(), Long.MAX_VALUE);
    assertEquals(deserializer.readWord(), Long.MIN_VALUE);
  }

  /** A smoke/fuzz test for ascending (from zero) word values. */
  @Test
  public void ascendingSmokeTest() {
    for (int wordLength = 5; wordLength < 65; wordLength++) {
      runAscendingTest(wordLength, 3 /*bytePadding, arbitrary*/, 100000 /*wordCount, arbitrary*/);
    }
  }

  /** A smoke/fuzz test for random word values. */
  @Test
  public void randomSmokeTest() {
    for (int wordLength = 5; wordLength < 65; wordLength++) {
      runRandomTest(wordLength, 3 /*bytePadding, arbitrary*/, 100000 /*wordCount, arbitrary*/);
    }
  }

  // ------------------------------------------------------------------------
  /**
   * Runs a test which serializes and deserializes random word values.
   *
   * @param wordLength the length of words to test
   * @param bytePadding the number of bytes padding the byte array
   * @param wordCount the number of word values to test
   */
  private static void runRandomTest(
      final int wordLength, final int bytePadding, final int wordCount) {
    final long seed = RandomizedTest.randomLong();
    final Random random = new Random(seed);
    final Random verificationRandom = new Random(seed);

    final long wordMask;
    if (wordLength == 64) {
      wordMask = ~0L;
    } else {
      wordMask = (1L << wordLength) - 1L;
    }

    final BigEndianAscendingWordSerializer serializer =
        new BigEndianAscendingWordSerializer(
            wordLength /*wordLength, arbitrary*/,
            wordCount,
            bytePadding /*bytePadding, arbitrary*/);

    for (int i = 0; i < wordCount; i++) {
      final long value = random.nextLong() & wordMask;
      serializer.writeWord(value);
    }

    final byte[] bytes = serializer.getBytes();

    final BigEndianAscendingWordDeserializer deserializer =
        new BigEndianAscendingWordDeserializer(wordLength, bytePadding, bytes);

    assertEquals(deserializer.totalWordCount(), wordCount);
    for (int i = 0; i < wordCount; i++) {
      assertEquals(deserializer.readWord(), (verificationRandom.nextLong() & wordMask));
    }
  }

  /**
   * Runs a test which serializes and deserializes ascending (from zero) word values.
   *
   * @param wordLength the length of words to test
   * @param bytePadding the number of bytes padding the byte array
   * @param wordCount the number of word values to test
   */
  private static void runAscendingTest(
      final int wordLength, final int bytePadding, final int wordCount) {
    final long wordMask;
    if (wordLength == 64) {
      wordMask = ~0L;
    } else {
      wordMask = (1L << wordLength) - 1L;
    }

    final BigEndianAscendingWordSerializer serializer =
        new BigEndianAscendingWordSerializer(
            wordLength /*wordLength, arbitrary*/,
            wordCount,
            bytePadding /*bytePadding, arbitrary*/);

    for (long i = 0; i < wordCount; i++) {
      serializer.writeWord(i & wordMask);
    }

    final byte[] bytes = serializer.getBytes();

    final BigEndianAscendingWordDeserializer deserializer =
        new BigEndianAscendingWordDeserializer(wordLength, bytePadding, bytes);

    assertEquals(deserializer.totalWordCount(), wordCount);
    for (long i = 0; i < wordCount; i++) {
      assertEquals(deserializer.readWord(), i & wordMask);
    }
  }
}
